package org.itsolutions.javaBackup.utils;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Scanner;

import org.itsolutions.javaBackup.logger.ELogLevel;
import org.itsolutions.javaBackup.logger.ILogger;
import org.itsolutions.javaBackup.logger.LoggerHelper;

/**
 * 
 * @author Tomas Fecko
 */
public final class IOUtils {

	private static final ILogger 	LOGGER = LoggerHelper.getLogger(IOUtils.class);
	private static final int 		MASK = 0xff;

	/**
	 * 
	 */
	private IOUtils() {
	}

	/**
	 * Methods waits until command arrives from stdin.
	 * @param pCommand command to wait for
	 */
	public static void waitForInputCommand(String pCommand) {
		Scanner readUserInput = new Scanner(System.in);
		LOGGER.log(ELogLevel.INFO, "Write " + pCommand + " to " + pCommand + "...");
		while (true) {
			String input = readUserInput.nextLine();
			if (input.equals(pCommand)) {
				LOGGER.log(ELogLevel.INFO, "Executing " + pCommand);
				break;
			} else {
				LOGGER.log(ELogLevel.WARN, "Not recognized command !!!");
			}
		}
	}

	/**
	 * 
	 * @param pObj object to be converted to byte array
	 * @return byte array
	 * @throws IOException {@link IOException}
	 */
	public static byte[] getBytesExternalizable(Externalizable pObj) throws IOException {
		ByteArrayOutputStream mBAOS = new ByteArrayOutputStream();
		ObjectOutputStream mOOS = new ObjectOutputStream(mBAOS);
		pObj.writeExternal(mOOS);
		mOOS.flush();
		mOOS.close();
		mBAOS.close();
		return mBAOS.toByteArray();
	}

	/**
	 * 
	 * @param pPropertyName property name
	 * @return property value
	 */
	public static String getProperty(String pPropertyName) {
		String property = System.getProperty(pPropertyName);
		String env = System.getenv(pPropertyName);

		if (property != null) {
			return property;
		} else if (env != null) {
			return env;
		} else {
			return null;
		}
	}

	/**
	 * 
	 * @param pObj object to be converted to byte array
	 * @return byte array
	 * @throws IOException {@link IOException}
	 */
	public static byte[] getBytes(Object pObj) throws IOException {
		ByteArrayOutputStream mBAOS = new ByteArrayOutputStream();
		ObjectOutputStream mOOS = new ObjectOutputStream(mBAOS);		
		mOOS.writeObject(pObj);
		mOOS.flush();
		mOOS.close();
		mBAOS.close();
		return mBAOS.toByteArray();
	}		

	/**
	 * 
	 * @param pObj object to be converted to byte array
	 * @return byte array
	 * @throws IOException {@link IOException}
	 */
	public static byte[] getBytesFromExternalizable(Externalizable pObj) throws IOException {
		ByteArrayOutputStream mBAOS = new ByteArrayOutputStream();
		ObjectOutputStream mOOS = new ObjectOutputStream(mBAOS);
		pObj.writeExternal(mOOS);
		mOOS.flush();
		mOOS.close();
		mBAOS.close();
		return mBAOS.toByteArray();
	}

	/**
	 * 
	 * @param <T> type of object, which is in byte array
	 * @param pSerializedObj object in byte array
	 * @param pClass class of obj
	 * @return object from byte array
	 * @throws IOException {@link IOException}
	 * @throws ClassNotFoundException {@link ClassNotFoundException}
	 */
	@SuppressWarnings("unchecked")
	public static <T> T getObject(byte[] pSerializedObj, Class<T> pClass) throws IOException, ClassNotFoundException {
		ByteArrayInputStream mBAIS = new ByteArrayInputStream(pSerializedObj);
		ObjectInputStream mOIS = new ObjectInputStream(mBAIS);
		Object o = mOIS.readObject();
		mOIS.close();
		mBAIS.close();
		if (o.getClass().equals(pClass)) {
			return (T) o;
		}
		return null; 
	}
	
	/**
	 * 
	 * @param <T> type of object, which is in byte array
	 * @param pSerializedObj object in byte array
	 * @param pEmptyObj newInstance of externalizable object / empty object
	 * @return object from byte array
	 * @throws IOException {@link IOException}
	 * @throws ClassNotFoundException {@link ClassNotFoundException}
	 */
	@SuppressWarnings("unchecked")
	public static <T> T getExternalizable(byte[] pSerializedObj, Externalizable pEmptyObj) throws IOException, ClassNotFoundException {
		ByteArrayInputStream bais = new ByteArrayInputStream(pSerializedObj);
		ObjectInputStream ois = new ObjectInputStream(bais);
		pEmptyObj.readExternal(ois);
		ois.close();
		bais.close();
		return (T) pEmptyObj; 
	}	

	/**
	 * 
	 * @param pSerializedObj object in byte array
	 * @return object from byte array
	 * @throws IOException {@link IOException}
	 * @throws ClassNotFoundException {@link ClassNotFoundException}
	 */
	public static Object getObject(byte[] pSerializedObj) throws IOException, ClassNotFoundException {
		ByteArrayInputStream bais = new ByteArrayInputStream(pSerializedObj);
		ObjectInputStream ois = new ObjectInputStream(bais);
		Object o = ois.readObject();
		ois.close();
		bais.close();
		return o;
	}

	/**
	 * Method should find first free port from starting port number on the number of attempts.
	 * @param pStartingPort starting port, from which the available port search is starting
	 * @param pNumberOfAttempts number of attempts for finding the free port
	 * @return free port number
	 * @throws IOException exception is thrown, if no ports are free in specified range
	 */
	public static int selectPort(int pStartingPort, int pNumberOfAttempts) throws IOException {
		int port = pStartingPort;
		while (port <= pStartingPort + pNumberOfAttempts) {
			try {
				ServerSocket socket = new ServerSocket(port);
				socket.close();
				return port;
			} catch (IOException e) {
				port++;
			}
		}
		throw new IOException("Unable to find an available port...");
	}

	/**
	 * 
	 * @param pLong long
	 * @return byte array
	 */
	public static byte[] longToByteArray(long pLong) {
		byte [] b = new byte[8];  
		for (int i = 0; i < 8; i++) {  
			b[7 - i] = (byte) (pLong >>> (i * 8));  
		} 
		return b;
	}

	/**
	 * 
	 * @param pInt interger
	 * @return byte array
	 */
	public static byte[] intToByteArray(int pInt) {
		byte [] b = new byte[4];  
		for (int i = 0; i < 4; i++) {  
			b[3 - i] = (byte) (pInt >>> (i * 8));  
		} 
		return b;
	}	

	/**
	 * 
	 * @param pShort short
	 * @return byte array
	 */
	public static byte[] shortToByteArray(short pShort) {
		byte [] b = new byte[2];  
		for (int i = 0; i < 2; i++) {  
			b[1 - i] = (byte) (pShort >>> (i * 8));  
		} 
		return b;
	}	

	/**
	 * 
	 * @param pChar char
	 * @return char in byte[]
	 */
	public static byte[] charToBytes(char pChar) {
		return new byte[] { (byte) (pChar & MASK), (byte) (pChar >> 8 & MASK) };
	}
	
	/**
	 * 
	 * @param pBytes bytes
	 * @return char
	 */
	public static char byteToChar(byte[] pBytes) {
		return (char) ((((pBytes[0] & MASK) <<  0) | ((pBytes[1] & MASK) <<  8)));
	}
	
	/**
	 * 
	 * @param pBoolean boolean to byte
	 * @return boolean in byte array
	 */
	public static byte[] booleanToBytes(boolean pBoolean) {
		if (pBoolean) {
			return new byte[]{Byte.MAX_VALUE};
		} else {
			return new byte[]{Byte.MIN_VALUE};
		}
	}

	/**
	 * 
	 * @param pBytes bytes
	 * @return boolean
	 */
	public static boolean bytesToBoolean(byte[] pBytes) {
		return Byte.MAX_VALUE == pBytes[0];
	}

	/**
	 * 
	 * @param pBoolean boolean
	 * @return byte
	 */
	public static byte booleanToByte(boolean pBoolean) {
		if (pBoolean) {
			return Byte.MAX_VALUE;
		} else {
			return Byte.MIN_VALUE;
		}
	}

	/**
	 *  
	 * @param pByte byte
	 * @return boolean
	 */
	public static boolean byteToBoolean(byte pByte) {
		return Byte.MAX_VALUE == pByte;
	}

	/**
	 *  
	 * @param pByte byte
	 * @return boolean
	 */
	public static boolean makeBoolean(byte pByte) {
		return Byte.MAX_VALUE == pByte;
	}

	/**
	 * 
	 * @param pBytes bytes
	 * @return long
	 */
	//CHECKSTYLE:OFF
	public static long bytesToLong(byte... pBytes) {
		return ((((long) pBytes[0] & MASK) << 56) |
				(((long) pBytes[1] & MASK) << 48) |
				(((long) pBytes[2] & MASK) << 40) |
				(((long) pBytes[3] & MASK) << 32) |
				(((long) pBytes[4] & MASK) << 24) |
				(((long) pBytes[5] & MASK) << 16) |
				(((long) pBytes[6] & MASK) <<  8) |
				(((long) pBytes[7] & MASK) <<  0));
	}

	public static long makeLong(byte pByte0, byte pByte1, byte pByte2, byte pByte3, byte pByte4, byte pByte5, byte pByte6, byte pByte7) {
		return ((((long) pByte0 & MASK) << 56) |
				(((long) pByte1 & MASK) << 48) |
				(((long) pByte2 & MASK) << 40) |
				(((long) pByte3 & MASK) << 32) |
				(((long) pByte4 & MASK) << 24) |
				(((long) pByte5 & MASK) << 16) |
				(((long) pByte6 & MASK) <<  8) |
				(((long) pByte7 & MASK) <<  0));
	}	

	/**
	 * 
	 * @param pBytes bytes
	 * @return int
	 */
	public static int bytesToInt(byte... pBytes) {
		return (int)((((pBytes[0] & MASK) << 24) |
				((pBytes[1] & MASK) << 16) |
				((pBytes[2] & MASK) <<  8) |
				((pBytes[3] & MASK) <<  0)));
	}

	/**
	 * 
	 * @param pBytes bytes
	 * @return int
	 */
	public static int makeInt(byte pByte0, byte pByte1, byte pByte2, byte pByte3) {
		return (int)((((pByte0 & MASK) << 24) |
				((pByte1 & MASK) << 16) |
				((pByte2 & MASK) <<  8) |
				((pByte3 & MASK) <<  0)));
	}	

	/**
	 * 
	 * @param pBytes bytes
	 * @return int
	 */
	public static short bytesToShort(byte... pBytes) {
		return (short) ((((pBytes[0] & MASK) <<  8) | ((pBytes[1] & MASK) <<  0)));
	}	

	/**
	 * 
	 * @param pBytes bytes
	 * @return int
	 */
	public static short makeShort(byte pByte0, byte pByte1) {
		return (short) ((((pByte0 & MASK) <<  8) | ((pByte1 & MASK) <<  0)));
	}	
	//CHECKSTYLE:ON

	/**
	 * 
	 * @param pFloat float number
	 * @return byte array
	 */
	public static byte[] floatToBytes(float pFloat) {
		return intToByteArray(Float.floatToRawIntBits(pFloat));
	}

	/**
	 * 
	 * @param pBytes bytes
	 * @return float
	 */
	public static float bytesToFloat(byte... pBytes) {
		return Float.intBitsToFloat(bytesToInt(pBytes));
	}

	
	
	/**
	 * 
	 * @param pDouble double number
	 * @return byte array
	 */
	public static byte[] doubleToBytes(double pDouble) {
		return longToByteArray(Double.doubleToRawLongBits(pDouble));
	}

	/**
	 * 
	 * @param pBytes bytes
	 * @return float
	 */
	public static double bytesToDouble(byte... pBytes) {
		return Double.longBitsToDouble(bytesToLong(pBytes));
	}	

	/**
	 * 
	 * @param pSrc src array
	 * @param pDest destination array
	 * @param pOffset starting position in dest array to write
	 * @return last position in destination byteBuffer 
	 */
	public static int addBytes(byte[] pSrc, byte[] pDest, int pOffset) {
		System.arraycopy(pSrc, 0, pDest, pOffset, pSrc.length);
		return pOffset + pSrc.length;
	}

	/**
	 * 
	 * @param pSrc src array
	 * @param pDest destination array
	 * @param pOffset starting position in dest array to write
	 * @return last position in destination byteBuffer 
	 */
	public static int addBytes(byte pSrc, byte[] pDest, int pOffset) {
		pDest[pOffset] = pSrc;
		return pOffset + 1;
	}

	/**
	 * 
	 * @param pArrays arrays
	 * @return last position in destination byteBuffer 
	 */
	public static byte[] addToOneArray(byte[]... pArrays) {
		int size = 0;
		for (byte[] array : pArrays) {
			size += array.length;
		}
		byte[] dest = new byte[size];
		int position = 0;
		for (byte[] array : pArrays) {
			position = addBytes(array, dest, position);
		}
		return dest;
	}
	
	/**
	 * 
	 * @param pArray array
	 * @param pDouble double
	 * @return array
	 */
	public static double[] addToArray(double[] pArray, double pDouble) {
		if (pArray == null) {
			pArray = new double[1];
			pArray[0] = pDouble;
		} else {
			double[] dest = new double[pArray.length + 1];
			System.arraycopy(pArray, 0, dest, 0, pArray.length);
			dest[dest.length - 1] = pDouble;
			pArray = dest;
		}
		return pArray;
	}

	/**
	 * 
	 * @param pSrc array to trim
	 * @param pLengthToCutOut to position
	 * @return trimmed byte array
	 */
	public static byte[] trimToPosition(byte[] pSrc, int pLengthToCutOut) {
		byte[] returnBytes = new byte[pLengthToCutOut];
		System.arraycopy(pSrc, 0, returnBytes, 0, pLengthToCutOut);
		return returnBytes;
	}

	/**
	 * 
	 * @param pArray1 array 1
	 * @param pArray2 array 2
	 * @return true if equals
	 */
	public static boolean equals(Object[][] pArray1, Object[][] pArray2) {
		if (pArray1 == pArray2) {
			return true;
		}
		if (pArray1 == null || pArray2 == null) {
			return false;
		}
		int length = pArray1.length;
		if (pArray2.length != length) {
			return false;
		}
		for (int i = 0; i < length; i++) {
			Object o1 = pArray1[i];
			Object o2 = pArray2[i];
			if (!(o1 == null ? o2 == null : Arrays.equals((Object[]) o1, (Object[]) o2))) {
				return false;
			}
		}
		return true;
	}
	
	/**
	 * 
	 * @param pShort short
	 * @return byte array
	 */
	public static int shortToUnsignedInt(short pShort) {
		//CHECKSTYLE:OFF - not a magic number
		return pShort & 0xffff;
		//CHECKSTYLE:ON
	}	
	
	/**
	 * 
	 * @param pShorts shorts
	 * @return short array in bytes
	 */
	public static byte[] toBytes(short[] pShorts) {
		if (pShorts == null) {
			return intToByteArray(Integer.MIN_VALUE);
		} else {
			if (pShorts.length == 0) {
				return intToByteArray(0);
			} else {
				ByteBuffer byteBuffer = ByteBuffer.allocate(pShorts.length * 2 + 4);
				byteBuffer.putInt(pShorts.length);
				for (short s : pShorts) {
					byteBuffer.putShort(s);
				}
				return byteBuffer.array();
			}
		}
	}
	
	/**
	 * 
	 * @param pBytes bytes
	 * @return shorts from bytes
	 */
	public static short[] shortsFromBytes(byte[] pBytes) {
		if (pBytes == null) {
			return null;
		} else if (pBytes.length == 4) {
			int count = bytesToInt(pBytes);
			if (count == Integer.MIN_VALUE) {
				return null;
			} else {
				return new short[0];
			}
		} else {
			ByteBuffer byteBuffer = ByteBuffer.wrap(pBytes);
			int size = byteBuffer.getInt();
			short[] returnShort = new short[size];
			for (int x = 0; x < size; x++) {
				returnShort[x] = byteBuffer.getShort();
			}
			return returnShort;
		}
	}
}














